/*******************************************************************************
 * Copyright 2009, Dmitri Livotov
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 ******************************************************************************/
package com.taysear.parcel.util.tpt;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.taysear.parcel.ParcelApplication;
import com.vaadin.Application;
import com.vaadin.terminal.StreamResource;
import com.vaadin.ui.Embedded;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.UUID;

/**
 * This component represents a captcha image that can be displayed for some form
 * validations. You can provide your own text for captcha image or let the
 * component to automatically generat it to you.
 */
@SuppressWarnings("serial")
public class Captcha extends Embedded implements StreamResource.StreamSource {

	/**
	 * Image provider for captcha
	 */
	private CaptchaImageProvider imageProvider = new DefaultCaptchaImageGenerator();

	/**
	 * Current captcha code
	 */
	private String captchaCode = "";
	ParcelApplication app = null;

	/**
	 * Creates a captcha component with random 5-letter code generated
	 */
	public Captcha(ParcelApplication app) {
		super();
		this.app = app;
		generateCaptchaCode(5);
	}

	/**
	 * Creates a captcha component with the specified text to be used as captcha
	 * code
	 * 
	 * @param code
	 *            captcha code
	 */
	public Captcha(String code) {
		super();
		setCaptchaCode(code);
	}

	/**
	 * Sets the new captcha code. Image will be regenerated automatically.
	 * 
	 * @param code
	 *            new captcha code
	 */
	public void setCaptchaCode(String code) {
		setCaptchaCode(code, app);
	}

	/**
	 * Sets the new captcha code. Image will be regenerated automatically. Use
	 * this method when you're using this widget without the using
	 * TPTApplication class.
	 * 
	 * @param code
	 *            new code to generate
	 * @param app
	 *            Vaadin application instance. Required to generate a new
	 *            StreamSource
	 */
	public void setCaptchaCode(String code, Application app) {
		captchaCode = code;
		refreshCaptchaImageSource(app);
	}

	/**
	 * Provides the current captcha code that is displayed in the component
	 * 
	 * @return current captcha code
	 */
	public String getCaptchaCode() {
		return captchaCode;
	}

	/**
	 * Verifies the given code agains current captcha code.
	 * 
	 * @param sample
	 *            sample text to compare with the current captcha component
	 * @return <code>true</code> if provided code matches the captcha (uses
	 *         case-insensitive comparison)
	 */
	public boolean verifyCaptchaCode(final String sample) {
		return sample != null && sample.equalsIgnoreCase(getCaptchaCode());
	}

	/**
	 * Generates and sets a random captcha code with the specified characters
	 * length.
	 * 
	 * @param charactersCount
	 *            number of characters in the new code
	 * @return generated code. Note, that this method will also set the newly
	 *         generated code to a component.
	 */
	public String generateCaptchaCode(int charactersCount) {
		setCaptchaCode(RandomPasswordGenerator.generate(charactersCount));
		return getCaptchaCode();
	}

	/**
	 * Sets the new image provider, that is responsible for creating captcha
	 * images. TPTCaptcha compnent has its own default image generator, but you
	 * may specify your own implementation if you wish.
	 * 
	 * @param provider
	 *            new image provider to use. Captcha code will be regenerated
	 *            using this new image provider immideately.
	 */
	public void setCaptchaImageProvider(CaptchaImageProvider provider) {
		setCaptchaImageProvider(provider, app);
	}

	/**
	 * Sets the new image provider, that is responsible for creating captcha
	 * images. TPTCaptcha compnent has its own default image generator, but you
	 * may specify your own implementation if you wish.
	 * 
	 * @param provider
	 *            new image provider to use. Captcha code will be regenerated
	 *            using this new image provider immideately.
	 */
	public void setCaptchaImageProvider(CaptchaImageProvider provider,
			Application app) {
		this.imageProvider = provider;
		refreshCaptchaImageSource(app);
	}

	public InputStream getStream() {
		BufferedImage image = imageProvider.getCaptchaImage("" + captchaCode);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(bos);

		try {
			encoder.encode(image);
		} catch (IOException e) {
			throw new RuntimeException("Captcha generation error: "
					+ e.getMessage(), e);
		}
		return new ByteArrayInputStream(bos.toByteArray());
	}

	private void refreshCaptchaImageSource(Application app) {
		setSource(new StreamResource(this, UUID.randomUUID().toString()
				+ ".jpg", app));
	}

	/**
	 * API for connecting custom image generators. Use setImageProvider method
	 * of the TPTCaptcha component to set the new image provider if you want the
	 * captcha images to be generated differently.
	 */
	public interface CaptchaImageProvider {

		/**
		 * Should provide a BufferedImage that represens the capctha code.
		 * 
		 * @param text
		 *            text to encode in the image
		 * @return encoded captcha image to be displayed
		 */
		BufferedImage getCaptchaImage(String text);
	}

	private class DefaultCaptchaImageGenerator implements Captcha.CaptchaImageProvider, Serializable {

		private static final int LETTER_WIDTH = 50;
		private static final int IMAGE_HEIGHT = 60;
		private static final double SKEW = 2.5;
		private static final int DRAW_LINES = 4;
		private static final int DRAW_BOXES = 1;
		private final Color[] RANDOM_BG_COLORS = { Color.RED, Color.CYAN,
				Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK,
				Color.YELLOW };
		private final Color[] RANDOM_FG_COLORS = { Color.BLACK, Color.BLUE,
				Color.DARK_GRAY };

		public BufferedImage getCaptchaImage(String code) {
			int MAX_LETTER_COUNT = code.length();
			int MAX_X = LETTER_WIDTH * MAX_LETTER_COUNT;
			int MAX_Y = IMAGE_HEIGHT;

			BufferedImage outImage = new BufferedImage(MAX_X, MAX_Y,
					BufferedImage.TYPE_INT_RGB);
			Graphics2D g2d = outImage.createGraphics();
			g2d.setColor(java.awt.Color.WHITE);
			g2d.fillRect(0, 0, MAX_X, MAX_Y);
			for (int i = 0; i < DRAW_BOXES; i++) {
				paindBoxes(g2d, MAX_X, MAX_Y);
			}

			Font font = new Font("dialog", 1, 33);
			g2d.setFont(font);

			g2d.setColor(Color.BLACK);
			g2d.drawRect(0, 0, (MAX_X) - 1, MAX_Y - 1);

			AffineTransform affineTransform = new AffineTransform();

			for (int i = 0; i < MAX_LETTER_COUNT; i++) {
				double angle = 0;
				if (Math.random() * 2 > 1) {
					angle = Math.random() * SKEW;
				} else {
					angle = Math.random() * -SKEW;
				}
				affineTransform.rotate(angle, (LETTER_WIDTH * i)
						+ (LETTER_WIDTH / 2), MAX_Y / 2);
				g2d.setTransform(affineTransform);
				setRandomFont(g2d);
				setRandomFGColor(g2d);
				g2d.drawString(code.substring(i, i + 1),
						(i * LETTER_WIDTH) + 3, 28 + (int) (Math.random() * 6));

				affineTransform.rotate(-angle, (LETTER_WIDTH * i)
						+ (LETTER_WIDTH / 2), MAX_Y / 2);
			}

			g2d.setXORMode(Color.RED);
			g2d.setStroke(new BasicStroke(1));
			g2d.drawLine(0, 0, MAX_X, MAX_Y);
			g2d.setXORMode(Color.YELLOW);
			g2d.drawLine(0, MAX_Y, MAX_X, 0);

			for (int i = 0; i < DRAW_LINES; i++) {
				g2d.setXORMode(Color.RED);
				g2d.setStroke(new BasicStroke(2));
				int y1 = (int) (Math.random() * MAX_Y);
				g2d.drawLine(0, y1, MAX_X, y1);

			}

			return outImage;
		}

		private void paindBoxes(Graphics2D g2d, int MAX_X, int MAX_Y) {
			int colorId = (int) (Math.random() * RANDOM_BG_COLORS.length);
			g2d.setColor(RANDOM_BG_COLORS[colorId]);
			g2d.fillRect(getRandomX(MAX_X), getRandomY(MAX_Y),
					getRandomX(MAX_X), getRandomY(MAX_Y));
		}

		private int getRandomX(int max_x) {
			return (int) (Math.random() * max_x);
		}

		private int getRandomY(int max_y) {
			return (int) (Math.random() * max_y);
		}

		private void setRandomFont(Graphics2D g2d) {
			Font font = new Font("dialog", 1, 33);
			g2d.setFont(font);
		}

		private void setRandomFGColor(Graphics2D g2d) {
			int colorId = (int) (Math.random() * RANDOM_FG_COLORS.length);
			g2d.setColor(RANDOM_FG_COLORS[colorId]);
		}
	}
}